#!/usr/bin/env python
# -*- mode: python; coding: utf-8 -*-

# Copyright (c) 2010, Óscar Aceña

# HanziTrainer is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# HanziTrainer is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with HanziTrainer.  If not, see <http://www.gnu.org/licenses/>.


import sys
import os
import random
import tempfile
import subprocess

import gobject
import gtk
import pango
import sqlite3 as sql


VERSION = "0.3.4"
MAX_POINTS = 500

class SortedDict(dict):
    '''A fixed-position dictionary. The keys will be stored on a list in the
    same order as are inserted.'''

    def __init__(self, pairs=[]):
        """pairs is a list or tuple of pairs (kay,value)"""
        self.__keylist = []
        for k,v in pairs:
            self[k] = v

    def __getitem__(self, key):
        return dict.__getitem__(self, key)

    def __setitem__(self, key, value):
        dict.__setitem__(self, key, value)
        if key in self.__keylist:
            self.__keylist.remove(key)
        self.__keylist.append(key)

    def update(self, other):
        for k,v in other.items():
            self[k] = v

    def keys(self):
        return self.__keylist

    def values(self):
        return [self[k] for k in self.__keylist]

    def items(self):
        return [(k, self[k]) for k in self.__keylist]

    def __iter__(self):
        return self.__keylist.__iter__()

    def clear(self):
        self.__keylist = []
        dict.clear(self)

    def __str__(self):
        ret = "{"
        for k,v in self.items(): ret += "'%s': '%s', " % (k,v)
        return ret[:-2] + "}"


class HTException(Exception):
    def __init__(self, msg):
        self._msg = msg

    def __str__(self):
        return self._msg


class DB:

    class AlreadyExists(HTException): pass

    def __init__(self, filename):
        conn = sql.connect(filename)
        conn.text_factory = str
        self._c = conn.cursor()
        self._setupDB()

        # True when some change done
        self.changed = False

    @property
    def _tbname(self):
        raise NotImplementedError, "Derived class must define this property"

    @property
    def _coldefs(self):
        raise NotImplementedError, "Derived class must define this property"

    def _setupDB(self):
        # 'id' is always present
        fields = "id integer primary key autoincrement, "
        for k,v in self._coldefs.items():
            fields += "%s %s, " % (k,v)
        fields = fields[:-2]

        self._c.execute("create table if not exists %s (%s)"
                        % (self._tbname, fields))

        # Note: do not commit here

    def __del__(self):
        if self._c:
            self._c.connection.commit()
            self._c.connection.close()

    # 'constraint' are the stringfied pairs (field=value) that must match to be
    # considered the same row. If None, all fields are to be match
    # example: constraint = "pinyin='nĭhăo' and hanzi='你好'"
    def addRow(self, data, constraint="", replace=False):
        # assure data is valid
        assert isinstance(data, dict), "data must be a dictionary"
        for k in data.keys():
            assert k in self._coldefs, "unknown column name '%s'" % k

        # create or complete constraint
        rows = []
        if not constraint:
            for k,v in data.items():
                constraint += k + "='" + str(v) + "' and "
            constraint = constraint[:-5]

        constraint = "where " + constraint
        query = "select * from %s %s" % (self._tbname, constraint)
        rows = self._c.execute(query)

        # NOTE: cursor.rowcount does not work with 'execute' :(
        # as I need it, this will do
        rows = list(rows)

        # replace?
        if len(rows) > 0:
            if not replace:
                raise DB.AlreadyExists(str(rows))

            # this will remove all other words (just the new one will remain)
            query = "delete from %s %s" % (self._tbname, constraint)
            self._c.execute(query)

        # new (or replaced) word, insert it
        keys = ",".join(data.keys())
        qmarks = ("?,"*len(data))[:-1]
        query = "insert into %s(%s) values (%s)" % (self._tbname, keys, qmarks)
        self._c.execute(query, data.values())

        self._c.connection.commit()
        self.changed = True

    def getRowsByCol(self, col, match):
        assert col in ["id"] + self._coldefs.keys(),\
            "Unknown column name '%s'" % col

        constraint = "where %s='%s'" % (col, match)
        query = "select * from %s %s" % (self._tbname, constraint)
        rows = self._c.execute(query)

        ret = []
        for row in rows:
            ret.append({})
            row = list(row)
            row.reverse()
            for k in ["id"] + self._coldefs.keys():
                ret[-1][k] = row.pop()

        return ret

    def save(self, filename):
        """Save current data to a file"""

        # connect to DB
        conn = sql.connect(filename)
        conn.text_factory = str
        c = conn.cursor()

        # create static
        fields = "id integer primary key autoincrement, "
        for k,v in self._coldefs.items():
            fields += "%s %s, " % (k,v)
        fields = fields[:-2]

        c.execute("create table if not exists %s (%s)"
                  % (self._tbname, fields))

        # fill with dynamic
        fields = "id," + ",".join(self._coldefs.keys())
        query = "select %s from %s" % (fields, self._tbname)
        rows = self._c.execute(query)

        for r in rows:
            qmks = ("?,"*len(r))[:-1]
            c.execute("insert into %s values(%s)" % (self._tbname, qmks), r)

        c.connection.commit()
        c.connection.close()

    def merge(self, other):
        """Add unique items of other database"""

        # connect to other DB
        conn = sql.connect(other)
        conn.text_factory = str
        c = conn.cursor()

        # iter over its rows
        fields = ",".join(self._coldefs.keys())
        query = "select %s from %s" % (fields, self._tbname)
        rows = c.execute(query)

        for r in rows:
            # build constraint
            constraint = "where "
            i = 0
            for k in self._coldefs.keys():
                constraint += "%s='%s' and " % (k,r[i])
                i += 1
            constraint = constraint[:-5]

            # search for coincidence (if any, do nothing)
            query = "select %s from %s %s" % (fields, self._tbname, constraint)
            if list(self._c.execute(query)):
                continue

            # otherwise, insert row
            newrow = {}
            i = 0
            for k in self._coldefs.keys():
                newrow[k] = r[i]
                i += 1

            self.addRow(newrow)
            self.changed = True


class SentencesDB(DB):

    def __init__(self, *args, **kwargs):
        self._tbname = "SENTENCES"
        self._coldefs = SortedDict((("hanzi","not null"),
                                    ("pinyin","not null"),
                                    ("spanish","not null")))
        DB.__init__(self, *args, **kwargs)


class WordsDB(DB):

    def __init__(self, *args, **kwargs):
        self._tbname = "WORDS"
        self._coldefs = SortedDict((("session","integer not null"),
                                    ("hanzi","not null"),
                                    ("pinyin","not null"),
                                    ("spanish","not null")))
        DB.__init__(self, *args, **kwargs)

    def getNSessions(self, n=None):
        """Return last n sessions (n > 0, 'None' will return all)"""

        constraint = ""
        if n is not None:
            assert n > 0
            last = self.getLastSession()
            constraint = " where session > %d" % (last - n)

        query = "select hanzi,pinyin,spanish from %s %s" % (self._tbname,
                                                            constraint)
        c = self._c.execute(query)

        for row in c:
            yield row

    def getLastSession(self):
        c = self._c.execute("select max(session) from %s" % self._tbname)
        ret = c.fetchone()[0]
        if ret is None: ret = 0
        return ret


class PersonalDB(DB):

    def __init__(self, *args, **kwargs):
        self._tbname = "USERDATA"
        self._coldefs = SortedDict((("wordid","int"),
                                    ("sentenceid","int"),
                                    ("points","int not null")))
        DB.__init__(self, *args, **kwargs)

        # This table may not be present. Commit to create an empty one if so
        self._c.connection.commit()

    def getLessPointedWordsIds(self, points=101, words=True, sentences=False):
        """Return ids of items which have less points than given"""
        assert words or sentences

        fields = ""
        if words: fields += "wordid,"
        if sentences: fields += "sentenceid,"
        fields = fields[:-1]

        query = ("select %s from %s where points < %d" %
                 (fields, self._tbname, points))
        c = self._c.execute(query)
        for row in c:
            yield row

    def getAllWordsPoints(self):
        query = "select points from %s" % self._tbname
        result = self._c.execute(query)
        return [r[0] for r in result.fetchall()]


class TwowayIter:

    def __init__(self, iterable):
        assert hasattr(iterable, "__getitem__"), "Use with indexable classes"
        self._src = iterable
        self._i = -1

    def next(self):
        try:
            ret = self._src[self._i + 1]
            self._i += 1
            return ret
        except IndexError:
            raise StopIteration

    # Note: only will return an item when pointing to second element (the
    # first element does not have a previous one)
    def prev(self):
        if (self._i - 1) < 0:
            raise StopIteration
        self._i -= 1
        return self._src[self._i]

    # These are non-standard methods
    @property
    def has_next(self):
        return (self._i + 1) < len(self._src)

    @property
    def has_prev(self):
        return (self._i - 1) >= 0

    @property
    def size(self):
        return len(self._src)

    @property
    def index(self):
        return self._i + 1


class UIMEasyGetter(gtk.Builder):

    def __init__(self, *args, **kwargs):
        gtk.Builder.__init__(self, *args, **kwargs)

    def __getattr__(self, name):
        try:
            return self.__dict__[name]
        except KeyError:
            widget = self.get_object(name)
            if widget:
                self.__dict__[name] = widget
            return widget


class HanziTrainer(object):

    def __init__(self, filename=None):
        # GUI things
        self._show_sp = True        # Show Spanish or Hanzi?
        self._input_pinyin = True   # Input on Pinyin or Hanzi?
        self._items = None

        self._ui = UIMEasyGetter()
        self._ui.add_from_file("hanzitrainer.ui")
        self._ui.connect_signals(self)

        self._ui.window.connect("delete-event", self.on_quit_activate)
        self._ui.window.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color("white"))

        self._ui.window.set_title("Hanzi Trainer v%s" % VERSION)
        self._ui.window.show_all()

        # set entry to scim input method
        self._ui.entry_hz.set_property("im-module", "scim")
        self._ui.entry_res.set_property("im-module", "scim")

        # font modifications
        self._ui.entry_res.modify_font(pango.FontDescription("bold 14"))

        # set folders to $HOME
        folder = os.path.expanduser("~")
        self._ui.ask_for_filename.set_current_folder(folder)
        self._ui.get_filename.set_current_folder(folder)

        # open file, if given:
        if filename:
            self._open_from_filename(filename)

        self.filename = filename
        self._reset()

    @property
    def filename(self):
        try: return self._filename
        except AttributeError: self._filename = None

    @filename.setter
    def filename(self, fname):
        self._filename = fname
        nt = self._ui.window.get_title().split(" ~ ")[0]

        if fname:
            if len(fname) > 30: nt += " ~ " + fname[:15] + "..." + fname[-15:]
            else:               nt += " ~ " + fname
        else:
            nt += " ~ *"

        self._ui.window.set_title(nt)

    @property
    def getUserInput(self):
        try: return self._getUserInput
        except AttributeError: self._getUserInput = None

    @getUserInput.setter
    def getUserInput(self, value):
        self._getUserInput = value
        if not self._items:
            return

        if value:
            self._ui.hbox_buttons.hide()
            self._ui.hbox_res.show()
        else:
            self._ui.hbox_buttons.show()
            self._ui.hbox_res.hide()

    # decorator to catch and show all HT Exceptions
    def showHTExceptions(fn):
        def deco(self, *args, **kwargs):
            try:
                return fn(self, *args, **kwargs)
            except HTException, e:
                self._ui.message_error.set_markup(str(e))
                self._ui.message_error.run()
                self._ui.message_error.hide()
        return deco

    # GUI callbacks
    def on_mi_save(self, widget=None):
        self._save_current_state()

        for db in [self._words, self._sentences, self._userdata]:
            db.changed = False

    def on_mi_save_as(self, widget):
        oldname = self.filename
        if not self._save_current_state():
            self.filename = oldname
            return

        for db in [self._words, self._sentences, self._userdata]:
            db.changed = False

    @showHTExceptions
    def on_mi_open(self, widget):
        if not self._discard_changes():
            return

        # select a file to open
        res = self._ui.get_filename.run()
        self._ui.get_filename.hide()
        if res != gtk.RESPONSE_ACCEPT:
            return

        filename = self._ui.get_filename.get_filename()
        if not filename: return

        self._open_from_filename(filename)

    def on_mi_new(self, widget):
        if not self._discard_changes():
            return

        self.filename = None
        self._open_from_filename(None)

    @showHTExceptions
    def on_mi_merge(self, widget):
        # select a file to merge with
        res = self._ui.get_filename.run()
        self._ui.get_filename.hide()
        if res != gtk.RESPONSE_ACCEPT:
            return

        filename = self._ui.get_filename.get_filename()
        if not filename: return

        # If given filename has no data (or invalid data), will be ignored
        for tbl in [self._words, self._sentences, self._userdata]:
            try: self._tbl.merge(filename)
            except sql.Error: pass

    def on_mi_show_input(self, widget):
        self.getUserInput = widget.get_active()
        self._ui.entry_res.grab_focus()

    @showHTExceptions
    def on_mi_sqliteman(self, widget):
        if not self.filename:
            raise HTException("No hay ficheros que explorar. Cree uno primero.")

        try:
            self._sqliteman = subprocess.Popen(["sqliteman", self.filename])
        except OSError:
            raise HTException("<b>sqliteman</b> no está instalado.\n"
                              "Debe instalarlo primero.")

    def on_all_words_mode(self, widget):
        return self._setup_items(list(self._words.getNSessions()))

    def on_30_words_mode(self, widget):
        return self._setup_items(list(self._words.getNSessions(30)))

    def on_7_words_mode(self, widget):
        return self._setup_items(list(self._words.getNSessions(7)))

    def on_3_words_mode(self, widget):
        return self._setup_items(list(self._words.getNSessions(3)))

    def on_unknown_words_mode(self, widget):
        return self._setup_items(list(self._getUnknownWords()))

    def on_sentence_mode(self, widget):
        return self._setup_items(list(self._getKnownSentences()))

    def on_button_next_clicked(self, widget=None):
        try:
            next = self._items.next()
        except StopIteration:
            return

        # last item?
        if not self._items.has_next:
            self._ui.button_next.set_sensitive(False)
        if self._items.has_prev:
            self._ui.button_prev.set_sensitive(True)

        self._updateUserInterface(next)

    def on_button_prev_clicked(self, widget=None):
        try:
            prev = self._items.prev()
        except StopIteration:
            return

        # last item?
        if not self._items.has_prev:
            self._ui.button_prev.set_sensitive(False)
        if self._items.has_next:
            self._ui.button_next.set_sensitive(True)

        self._updateUserInterface(prev)

    def on_toggle_show_translation(self, widget):
        self._show_sp = widget.get_active()

        # Update visibility
        if self._items is not None:
            if self._show_sp:
                self._ui.sp_align.show()
                self._ui.hz_align.hide()
            else:
                self._ui.sp_align.hide()
                self._ui.hz_align.show()

            self._ui.py_align.hide()

    def on_learnt_yes(self, widget):
        self._set_current_as_learnt(True)
        self.on_button_next_clicked()

    def on_learnt_no(self, widget):
        self._set_current_as_learnt(False)
        self.on_button_next_clicked()

    def on_entry_session_activate(self, widget):
        self._ui.entry_hz.grab_focus()

    def on_entry_hz_activate(self, widget):
        self._ui.entry_py.grab_focus()

    def on_entry_py_activate(self, widget):
        self._ui.entry_sp.grab_focus()

    def on_entry_sp_activate(self, widget):
        self._ui.dialog_add.response(gtk.RESPONSE_OK)

    def on_entry_clear(self, widget, iconpos=None, event=None):
        widget.set_text("")

    def on_button_show_toggled(self, widget):
        if widget.get_active():
            self._showFullCard()
        else:
            self._hideFullCard()

    def on_add_word(self, widget):
        spin = self._ui.entry_session
        spin.set_text(str(self._words.getLastSession()))
        spin.grab_focus()

        self._ui.entry_sp.set_text("")
        self._ui.entry_py.set_text("")
        self._ui.entry_hz.set_text("")

        self._ui.dialog_add.run()

    def on_add_sentence(self, widget):
        pass

    def on_input_validate(self, widget):
        data = self._ui.entry_res.get_text().replace(" ", "")
        if not data: return

        if self._input_pinyin:
            data = toLower(data)
            correct = toLower(self._current_word[1].replace(" ", ""))
        else:
            correct = self._current_word[0]

        self._set_current_as_learnt(correct == data)
        self._giveFeedback(correct == data)

    def on_input_type(self, widget):
        self._input_pinyin = not self._input_pinyin
        text = "Pīnyīn:" if self._input_pinyin else "汉字:"
        self._ui.bt_input_type.get_child().set_text(text)

    @showHTExceptions
    def on_add_dlg_response(self, dlg, response):
        dlg.hide()
        err_msg = None

        if response != gtk.RESPONSE_OK:
            return

        hanzi = self._ui.entry_hz.get_text().strip()
        pinyin = self._ui.entry_py.get_text().strip()
        spanish = self._ui.entry_sp.get_text().strip()

        try:
            session = int(self._ui.entry_session.get_text())
        except ValueError:
            raise HTException("'sessión' debe ser un número entero positivo.")

        if not hanzi or not pinyin or not spanish:
            raise HTException("Ningún campo puede estar vacío")

        self._add_to_session(session, hanzi, pinyin, spanish)

    def on_quit_activate(self, widget, event=None):
        if not self._discard_changes():
            return True

        if hasattr(self, "_sqliteman"):
            try: self._sqliteman.terminate()
            except: pass

        gtk.main_quit()

    def on_show_toolbar_toggled(self, widget):
        self._ui.toolbar.show() if widget.get_active() else self._ui.toolbar.hide()

    # Private methods
    def _discard_changes(self):
        if all((hasattr(self, "_words"),
                hasattr(self, "_sentences"),
                hasattr(self, "_userdata"))):

            if any((self._words.changed,
                    self._sentences.changed,
                    self._userdata.changed)):

                self._ui.ask_question_msg.set_text(
                    "Hay cambios que no se han guardado.\n"
                    "Si continua, se perderán los cambios. ¿Desea continuar?")
                res = self._ui.ask_question.run()
                self._ui.ask_question.hide()
                if res != gtk.RESPONSE_YES:
                    return False
        return True

    def _updateUserInterface(self, row):
        self._current_word = row

        self._ui.py_align.hide()
        self._ui.button_show.set_active(False)
        if self._show_sp:
            self._ui.sp_align.show()
            self._ui.hz_align.hide()
        else:
            self._ui.hz_align.show()
            self._ui.sp_align.hide()

        self._ui.sp_label.set_text(row[2])
        self._ui.py_label.set_text(row[1])
        self._ui.hz_label.set_markup("<span font='70'>"+row[0]+"</span>")

        if self.getUserInput:
            self._ui.entry_res.modify_base(gtk.STATE_NORMAL,
                                           gtk.gdk.Color("white"))
            self._ui.entry_res.set_text("")

        msg = "%d / %d" % (self._items.index, self._items.size)
        self._setStatusMessage(msg)

    def _setStatusMessage(self, message):
        cid = self._ui.statusbar.get_context_id("")
        self._ui.statusbar.pop(cid)
        self._ui.statusbar.push(cid, message)

    def _add_to_session(self, session, hanzi, pinyin, spanish):
        try:
            self._words.addRow({"session":session, "hanzi":hanzi,
                                "pinyin":pinyin, "spanish":spanish})
        except DB.AlreadyExists, e:
            self._ui.ask_question_msg.set_markup(
                "Parece que esa palabra ya había sido añadida:\n\n"
                "  <b>%s</b> (<b>%s</b>)\n\n"
                "¿Deseas reemplazarla?" % (hanzi, pinyin))

            res = self._ui.ask_question.run()
            self._ui.ask_question.hide()

            if res == gtk.RESPONSE_YES:
                self._words.add_row(session, hanzi, pinyin, spanish,
                                 replace=True)

    def _reset(self):
        self._ui.sp_align.hide()
        self._ui.hz_align.hide()
        self._ui.py_align.hide()

        self._ui.msg_align.show()
        self._ui.msg_label.set_text("¡Elige una acción!")

        self._ui.hbox_buttons.hide()
        self._ui.button_next.set_sensitive(False)
        self._ui.button_prev.set_sensitive(False)
        self._ui.button_show.set_sensitive(False)

        self._items = None

        cid = self._ui.statusbar.get_context_id("")
        self._ui.statusbar.pop(cid)

    def _setup_items(self, items):
        self._reset()
        if not items:
            self._ui.msg_label.set_text(
                "Lo siento, pero no hay elementos que mostrar.\n"
                "Utilice las opciones del menú para añadir alguno.")
            return

        random.shuffle(items)
        self._items = TwowayIter(items)

        # Setup gui
        self._ui.msg_align.hide()
        self.on_button_next_clicked()
        self.getUserInput = self.getUserInput # Force an update

        if self._items.has_next:
            self._ui.button_next.set_sensitive(True)

        self._ui.button_prev.set_sensitive(False)
        self._ui.button_show.set_sensitive(True)

    def _save_current_state(self):
        assert all((hasattr(self, "_words"),
                    hasattr(self, "_sentences"),
                    hasattr(self, "_userdata")))

        # if no filename selected, show dialog to choose a name
        if not self.filename:
            res = self._ui.ask_for_filename.run()
            self._ui.ask_for_filename.hide()
            if res != gtk.RESPONSE_ACCEPT:
                return False

            self.filename = self._ui.ask_for_filename.get_filename()

        # first, if file exists, remove it
        if os.path.exists(self.filename):
            os.remove(self.filename)

        # with filename, save on this filename
        self._words.save(self.filename)
        self._sentences.save(self.filename)
        self._userdata.save(self.filename)
        return True

    @showHTExceptions
    def _open_from_filename(self, filename=None):
        if filename:
            if not os.path.exists(filename):
                raise HTException("El fichero especificado no existe:\n"
                                  "<b>%s</b>" % filename)

        # make a temporary database
        tmp = tempfile.mktemp(prefix=".ht_")

        # open temporary file
        self._words = WordsDB(tmp)
        self._sentences = SentencesDB(tmp)
        self._userdata = PersonalDB(tmp)

        # if filename given, merge it with temporary
        if filename:
            try:
                self._words.merge(filename)
                self._sentences.merge(filename)
            except sql.Error, e:
                raise HTException("¡El fichero proporcionado no es válido!\n\n"
                                  "Detalles del error: " + str(e))

            # file may have not "USERDATA" table
            try: self._userdata.merge(filename)
            except sql.Error, e: pass

            for db in [self._words, self._sentences, self._userdata]:
                db.changed = False

            # and store it for future savings
            self.filename = filename

        # update gui
        for i in ("mi_save", "mi_save_as", "mi_merge", "mi_edit",
                  "mi_actions", "mi_view"):
            getattr(self._ui, i).set_sensitive(True)

        self._reset()

    def _set_current_as_learnt(self, isLearnt):
        wid = self._words.getRowsByCol("hanzi", self._current_word[0])[0]["id"]
        ud = self._userdata.getRowsByCol("wordid", wid)

        if not ud: points = 1
        else:      points = ud[0]["points"]

        points = points + 100 if isLearnt else int(points / 3)
        row = {"wordid":wid, "points":min(MAX_POINTS, points)}
        self._userdata.addRow(row, "wordid=%d" % wid, replace=True)

    def _giveFeedback(self, isGood):
        if isGood: self._giveGoodFeedback()
        else:      self._giveBadFeedback()

    def _giveGoodFeedback(self):
        self._ui.entry_res.modify_base(gtk.STATE_NORMAL,
                                       gtk.gdk.Color("#C1FFC1"))
        self._ui.button_show.set_active(True)

        def setNextWord():
            self._ui.entry_res.modify_base(gtk.STATE_NORMAL,
                                           gtk.gdk.Color("white"))
            self._ui.entry_res.set_text("")
            self.on_button_next_clicked()

        gobject.timeout_add(2000, setNextWord)

    def _giveBadFeedback(self):
        userInput = unicode(self._ui.entry_res.get_text(), "utf-8")
        position = self._getFirstTypoPosition(userInput)

        if position >= len(userInput):
            self._ui.entry_res.set_text(userInput+" ")

        self._ui.entry_res.select_region(position, position + 1)
        self._ui.entry_res.modify_base(gtk.STATE_NORMAL,
                                       gtk.gdk.Color("#FFE4E1"))

    def _getFirstTypoPosition(self, userInput):
        if self._input_pinyin:
            correctWord = self._current_word[1]
        else:
            correctWord = self._current_word[0]
        correctWord = unicode(correctWord, "utf-8").replace(" ", "")

        index = 0
        spaces = 0
        for c in userInput:
            if c in (" ", "\t", "\n"):
                spaces += 1
                continue
            if index >= len(correctWord): return index + spaces
            if c != correctWord[index]: return index + spaces
            index += 1
        return index + spaces

    def _showFullCard(self):
        self._ui.sp_align.show()
        self._ui.hz_align.show()
        self._ui.py_align.show()

    def _hideFullCard(self):
        self._ui.py_align.hide()
        if self._show_sp:
            self._ui.hz_align.hide()
        else:
            self._ui.sp_align.hide()

    def _getUnknownWords(self):
        points = self._userdata.getAllWordsPoints()
        total = 0
        for i in points: total += i
        avg = total / len(points)
        hist = avg * 0.15

        wordIds = self._userdata.getLessPointedWordsIds(avg - hist)
        for wid in wordIds:
            row = self._words.getRowsByCol("id", wid[0])[0]
            yield (row["hanzi"], row["pinyin"], row["spanish"])

    def _getKnownSentences(self):
        pass


def toLower(src):
    return unicode(src, "utf-8").lower()


if __name__ == "__main__":
    filename = sys.argv[1] if len(sys.argv) > 1 else None
    HanziTrainer(filename)

    try:
        gtk.main()
    except KeyboardInterrupt:
        pass
